fix(core,cli): parent-relative figma child geometry; groups stop rejecting subcommand flags#2063
Conversation
miga-heygen
left a comment
There was a problem hiding this comment.
LGTM with nits
Two real bugs, correctly root-caused, with good test coverage. The parent-relative geometry fix is particularly well done — the before/after analysis in the PR description makes the failure mode crystal clear.
Findings
motionContextToDocs.ts — regex key injection (nit, low risk)
arrayAfterKey and scalarAfterKey interpolate key into new RegExp(...) without escaping. Currently safe (keys are \w+ property names from the snippet regex), but a stray metacharacter in a future key would silently misparse. Worth a one-line escape or a comment noting the invariant.
verify-motion.mjs:80 — shell injection via execSync (nit, low risk)
const err = execSync(`ffmpeg -i ${JSON.stringify(a)} -i ${JSON.stringify(b)} ...`).toString();JSON.stringify isn't shell escaping — filenames with $() or backticks would be interpreted. Since this is an agent-facing tool (not user-facing), practical risk is low, but execFileSync with an array (already used elsewhere in the file) would close it.
Summary
- nodeToHtml parent-relative fix: correct, well-tested —
boxOf(node) ?? parentBoxcascading is the right pattern for CSS absolute positioning semantics - CLI flag rejection skip for command groups: correct, three-case test matrix covers leaf-rejects / group-delegates / group-not-delegating
- motionContextToDocs: solid mechanical parser, good wrap-stripping logic, tests cover the interesting edge cases
- verify-motion.mjs: useful calibration tool, minor hardening opportunity
Ship it.
— Miga
miguel-heygen
left a comment
There was a problem hiding this comment.
Spot-check matches Miga's code read: packages/core/src/figma/nodeToHtml.ts:194 now uses the parent box for absolute child geometry, which matches CSS positioning semantics, and packages/cli/src/utils/command-failure-tracking.ts:40 skips group-level flag validation only when the first positional delegates to a known subcommand. The targeted tests cover both regression shapes.
Not stamping yet for procedural state: GitHub reports mergeable_state=dirty / mergeable=false, and the head currently only has WIP + Mintlify status checks. There is no trustworthy full CI result to approve through until the branch is rebased/conflicts are resolved and CI reruns. Miga's two nits on motionContextToDocs.ts:79 regex key interpolation and skills/figma/scripts/verify-motion.mjs:75 execSync shelling are valid but non-blocking from my read.
Verdict: COMMENT
Reasoning: Code shape looks approval-quality in the diff, but I can't stamp a dirty branch with no full CI run on the head.
— Magi
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at f883064.
Two well-scoped bug fixes, both correctly root-caused, plus a handy pair of infrastructure adds (motionContextToDocs, verify-motion.mjs) that materialise the field notes from the SDS brand-loop walkthrough into reusable helpers. PR body's before/after narrative is exemplary — I could reproduce the parent-relative reasoning without opening a single file, then verify it in the diff and it matched. Nothing 🔴 from my read; one 🟠 worth surfacing on the CLI fix, plus a couple of nits already landed on by Miga.
Layering on Miga's LGTM + nits and Magi's COMMENT-not-approve (procedural: mergeable_state=dirty on head f883064, only Mintlify + WIP checks on the SHA in statusCheckRollup — the earlier SHA 8530f6b did get a full CI green (CI / CodeQL / Windows render / preview-regression / Player perf / regression, all success, pull_request event), but that green is stale for the current head).
Core geometry (parent-relative figma children)
🟢 Correctness
nodeToHtml.ts:194-217 — the fix is right. boxOf(node) ?? parentBox cascading in renderChildren at :296 is the crucial piece: each depth's child subtracts its immediate positioned ancestor's absolute box, matching CSS position: absolute's "nearest positioned ancestor" semantics. The ?? parentBox fallback handles intermediate nodes with no bounding box correctly — since those nodes emit a <div> with no position set, CSS resolves the grandchild's absolute position against the next positioned ancestor up, and passing the grandparent's box down mirrors that. Root's origin default ({x:0,y:0,width:0,height:0} when the root has no box) means depth-1 children under a boxless root render at absolute canvas coords, which is the only sensible fallback.
Regression test at nodeToHtml.test.ts:52-89 asserts the specific integer values (left: 20px, top: 40px) AND explicitly rejects the double-offset shape (.not.toContain("left: 520px")), which is exactly the anti-test I want for a geometry fix — it can't accidentally pass a fixture regeneration.
🟢 Bonus fix (undocumented in PR body)
uniqueSlug at nodeToHtml.ts:164-176 — the digit-leading-slug prefix (n + slug when the slugified name starts with 0-9) is a real fix that's easy to miss in a diff scan. Test at :87-100 covers it. Worth calling out in the PR body next time — anyone rebasing on top of this and slugifying figma names elsewhere would want to know the invariant.
🟡 Nits (both already flagged by Miga)
motionContextToDocs.ts:79-80,85—arrayAfterKey/scalarAfterKeyinterpolatekeyintonew RegExp(...)unescaped. Safe today because all callers pass\w+property names (times,duration,ease, or property names harvested by/(\w+)\s*:\s*\[/g), but a one-line comment noting the invariant, or aRegExp.escape-shaped helper, would prevent a future caller from tripping it. Miga's suggestion.motionContextToDocs.tsbalancedBlockdoesn't skip over string bodies — a}inside a JSX string prop value would break brace-depth tracking. motion.dev's output is well-behaved (bezier arrays, named eases, no arbitrary strings), so this is a "future-proofing" note more than a bug. Fine as-is.
CLI groups (subcommand-flag rejection)
🟢 Correctness of the delegation-detection
command-failure-tracking.ts:40-47 — the shape is right. Group with subCommands set + a first-positional token that names a known subcommand ⇒ skip group-level assertKnownFlags. Three-case test matrix (leaf rejects, group delegates, group not delegating still rejects) covers the intent well.
🟠 Real concern — subcommand-level flag typos now silently ignored
Tracing this end-to-end: only the top-level commandLoaders in cli.ts:154-159 are wrapped by trackCommandFailures. The figma subcommand loaders defined in commands/figma.ts:52-57 (asset, tokens, component) are NOT wrapped. Under the OLD code, if you ran hyperframes figma component KEY:1-2 --bogus-flag, citty silently ignored --bogus-flag at the component leaf (per the whole reason reject-unknown-flags.ts exists), then the group-level wrapper caught it via its own assertKnownFlags — a false-positive on real flags (--name, per the PR body) but a true-positive on typos.
The fix removes the false-positive by skipping the group check on delegation. But it also removes the incidental true-positive protection at the leaf: hyperframes figma component KEY:1-2 --bogus now runs to completion with --bogus silently dropped. That's the exact class of silent-wrong-result that assertKnownFlags was written to prevent (see the docstring at reject-unknown-flags.ts:3-7) — the PR reintroduces it for every figma subcommand.
Practical example that would now silently succeed: hyperframes figma component KEY:1-2 --namee hero (typo of --name). Under the old code the group wrapper would have errored Unknown flag: --namee, giving you the typo hint. Under the new code, the import runs with the default component name — same class of "silent wrong result" the reject module set out to catch.
Suggested follow-up (doesn't have to block this PR — I'd take a TODO(vi): comment + a follow-up ticket): apply the same tracker wrapping inside commands/figma.ts for each subcommand loader. Roughly:
subCommands: {
asset: trackCommandFailures(
() => import("./figma/asset.js").then((m) => m.default),
(err) => reportCommandFailure("figma asset", err),
),
// ...
}Or lift the wrapping into a small wrapSubCommands(name, loaders) helper that both cli.ts and each group can use.
If you already thought about this and decided the leaf-level typo protection isn't worth the wiring cost, a one-line note in the block-comment at :33-39 documenting that decision would save the next reader (or future you) a repeat trace.
🟡 First-positional heuristic edge case
command-failure-tracking.ts:42 — firstPositional = rawArgs.find((tok) => tok && !tok.startsWith("-")) treats any non-dash token as a positional. If a group ever grows a boolean flag whose value token happens to match a subcommand name (figma --level component KEY:1-2 --name x where --level is bool + component is its value in the shell's read order), the heuristic incorrectly infers delegation and skips validation. No group has flags today, so this is dormant, but a one-line comment noting the invariant ("this heuristic is sound only while command groups declare no flags of their own; if a group grows a flag, use a proper argv parse here") would keep the next contributor from silently invalidating it. Same nit shape as the regex-key one — worth surfacing to make the invariant load-bearing rather than incidental.
motionContextToDocs.ts + verify-motion.mjs
Both are additive infrastructure (no risk to existing paths) and both are well-shaped:
-
motionContextToDocs.ts— theWRAP_EPSILON_S = 0.005heuristic is defensible with the calibration in the header comment (SDS "Unlocked" card), tests exercise the four interesting shapes (single wrap marker, multi-keyframe wrap cluster, hold segment, multi-property node), and preserving bezier eases verbatim is right. TheselectorForcallback pattern correctly forces the caller to supply Phase-3-import ids (a nice API affordance — the alternative of deriving fromnodeNamewould silently drift). -
verify-motion.mjs— motion-energy delta comparison is a smart way to isolate choreography from static-fidelity noise. Calibration numbers in the header comment (faithful ≈ 20+dB, diverging ≈ 5dB, threshold 15) give the reader something concrete to reason about. Miga'sexecSync→execFileSyncswap at:80is a legitimate hardening opportunity (JSON.stringifyisn't shell escaping —$()and backticks in a filename would be interpreted). Paths are allmkdtempSync-generated in practice so today's risk is zero, but the rest of the file already usesexecFileSyncwith array args — trivial to align this one call.
Doc / manifest
skills/figma/SKILL.md:88-90 — updates the phase-2 write-up to point at the new mechanical helper and the verify script, both accurately. skills-manifest.json hash bump is consistent with the added file. LGTM.
Bundling
Both fixes surfaced in the same brand-loop walkthrough and touch the figma-mapper lane end-to-end (component import → geometry → motion translation → verification). Coherent as one PR — I wouldn't push to split.
What I didn't verify
- Whether the digit-leading-slug prefix (
nprefix) collides with any existing DOM id convention or authored HTML in other imported components — unlikely, but not exhaustively grepped. - Whether
parseNodeTracks's innerwhile ((m = propRe.exec(animate)) !== null)atmotionContextToDocs.ts:177handles a motion.dev output where a property name repeats in a nested transition block (I don't think motion.dev emits that shape, but the regex is naive w.r.t. block structure — as noted, real-world safe). - No local test-suite run — trusted the PR body's 96/96 core + 431/431 cli numbers plus the earlier-SHA CI green.
…cting subcommand flags Both found running the brand-loop guide end-to-end against the Simple Design System: - nodeToHtml subtracted the ROOT origin from every node's absolute bounds, but CSS absolute positioning resolves against the nearest positioned ancestor — every nesting level re-added its ancestors' offsets, drifting nested content down-right and pushing deep children off-frame (hero buttons invisible, pricing grid collapsed to one card). Children now subtract their PARENT's box; regression test with a two-level tree. - trackCommandFailures asserted unknown flags against the command group's own (flagless) arg table even when the group was delegating to a subcommand, so `figma component <ref> --name x` imported and THEN threw "Unknown flag: --name". The assertion is now skipped when the first positional names a subcommand; leaf and non-delegating behavior is unchanged and covered by tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
slugify("3D Object - Headphones") produced id="3d-object-headphones" —
valid HTML, but querySelector("#3d-…") throws (CSS idents cannot start
with a digit), which kills GSAP targeting and figma-motion translation
against imported components. uniqueSlug now prefixes digit-leading slugs
("n3d-object-headphones"). Found translating a real Figma Motion timeline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… export_video validation Field lesson from translating a real Motion timeline: the two returned encodings window durations differently, and keyframes at times ~0.9999 are loop-wrap resets, not authored motion. Hand-normalizing across encodings and inventing visible returns produced a render that diverged from Figma. The skill now mandates verbatim single-encoding translation, wrap-via-repeat, and a frame-grid comparison against export_video ground truth before completion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…elity gate Two guarantees so figma-motion imports can't drift from the design again: - motionContextToDocs(): raw get_motion_context response -> MotionDoc[], in code. Parses the motion.dev snippets (the reliable encoding; the CSS snippets stretch durations and can disagree), strips loop-wrap tail keyframes (sub-ms segments at the window end are the loop reset, not authored motion), preserves bezier eases verbatim. Fixture test uses the verbatim response from a real Motion timeline whose translation was frame-validated against Figma's own export_video render. - skills/figma/scripts/verify-motion.mjs: mandatory post-render gate. Compares motion-energy deltas between the render and the export_video ground truth so static import fidelity cancels out and the score isolates choreography. Calibrated on a faithful translation (min 20.3dB) vs a diverging one (min 5.0dB); threshold 15dB. The skill's Motion step now routes through both: no hand transcription, no unverified completion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hell psnr probe - motionContextToDocs: escape regex metacharacters in arrayAfterKey / scalarAfterKey key interpolation (safe today for \\w+ keys; now safe for any future caller), and document balancedBlock's no-strings invariant. - verify-motion.mjs: execSync shell string -> spawnSync with array args (JSON.stringify is not shell escaping); verifier re-calibrated unchanged (faithful render still PASS at min 20.30dB). - command-failure-tracking: rebase folded the group-delegation skip into upstream's recursive wrapCommand (HF#2033) — leaf commands now assert their own flag tables, so `figma component --namee` is rejected at the leaf while `--name` passes the group; heuristic invariant documented. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f883064 to
3d59dcc
Compare
|
All feedback addressed in 3d59dcc (rebased onto v0.7.45 — the branch was dirty against main, per @miguel-heygen; fresh CI is running on the new head). @miga-heygen nits — both taken:
@james-russo-rames-d-jusso 🟠 (leaf-level typo protection) — resolved structurally by the rebase: main's HF#2033 landed recursive
🟡 first-positional heuristic — invariant documented in the block comment: sound only while command groups declare no flags of their own; grow a group flag → replace with a real argv parse. Suites post-rebase: core figma 104/104, cli flag/tracking tests 18/18 (merged upstream + this PR's three-case matrix). |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
miguel-heygen
left a comment
There was a problem hiding this comment.
Re-review at 666b84d7.
The prior procedural blocker is gone: the branch is rebased/mergeable, and the current head has full CI running with no red checks observed. Core checks including Build, Typecheck, Test, CLI smoke, Lint/Format, Skills manifest, Preview parity, and perf gates are green at review time; long regression shards / Windows / JS analysis are still pending and left to branch protection.
The substantive prior findings are addressed:
- Parent-relative Figma geometry is still the right fix:
geometryCsssubtracts the immediate parent box for non-root nodes (packages/core/src/figma/nodeToHtml.ts:194), and children passboxOf(node) ?? parentBoxdownward (packages/core/src/figma/nodeToHtml.ts:296). The regression test pins the exact nested offsets and anti-asserts the old root-relative double-offset (packages/core/src/figma/nodeToHtml.test.ts:55). - The CLI flag concern is fixed structurally.
wrapCommandnow recursively wraps subcommands (packages/cli/src/utils/command-failure-tracking.ts:29), while the group-level skip applies only when the first positional delegates to a known subcommand (packages/cli/src/utils/command-failure-tracking.ts:58). That preserves leaf typo protection. I verified this directly against the CLI source after building core:figma component KEY:1-2 --namee heronow errorsUnknown flag: --namee, while--name heropasses flag validation and only fails on missingFIGMA_TOKEN. - Miga's regex nit is fixed with
escapeRegExpand both dynamic regex sites now use it (packages/core/src/figma/motionContextToDocs.ts:61,packages/core/src/figma/motionContextToDocs.ts:88,packages/core/src/figma/motionContextToDocs.ts:104). The balanced-block string-literal invariant is documented (packages/core/src/figma/motionContextToDocs.ts:67). - Miga's shelling nit is fixed: the PSNR probe now uses
spawnSync("ffmpeg", [...]), no shell interpolation (skills/figma/scripts/verify-motion.mjs:110).
Local verification: command-failure-tracking.test.ts 11/11, nodeToHtml.test.ts + motionContextToDocs.test.ts 21/21, plus the direct CLI typo/valid-flag smoke described above.
No remaining blockers from my prior review.
— Magi
Verdict: APPROVE
Reasoning: The branch now has the full CI surface running, the earlier dirty-branch gate is resolved, and the CLI leaf-flag regression plus both nits have been fixed and verified at source and behavior level.
Both bugs surfaced while executing the brand-loop guide end-to-end against Figma's Simple Design System (follow-up to #2053).
1. Figma mapper: nested children were positioned root-relative
nodeToHtmlsubtracted the ROOT frame's origin from every node's absolute bounds. CSS absolute positioning resolves against the nearest positioned ancestor, so every nesting level re-added its ancestors' offsets — nested content drifted down-right and deep children left the frame entirely. On real SDS frames: hero buttons invisible, three-card pricing grid collapsed to a single off-center card.Children now subtract their parent's box. Regression test with a two-level tree asserts parent-relative coords and rejects the double-offset values.
Before/after on the same SDS import (Hero Actions, Card Grid Pricing):
2. CLI: command groups rejected their subcommands' flags
trackCommandFailuresrunsassertKnownFlagson every wrappedrun. A command group likefigma(subCommands + fallback-help run) asserted the raw args against its own flagless table, sohyperframes figma component <ref> --name xcompleted the import and THEN threwUnknown flag: --name. The check is now skipped when the first positional names a subcommand; leaf commands and non-delegating group invocations still reject unknown flags (all covered by new tests).Validation
🤖 Generated with Claude Code